/**
 *
 */
package org.windowkit.toolkit.widget;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DragSourceMotionListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JComponent;
import javax.swing.JTree;
import javax.swing.Timer;
import javax.swing.tree.TreePath;

import org.windowkit.toolkit.widget.tree.CheckBoxTree;

public class DnDIconPainter extends JComponent
        implements DragSourceListener, DragSourceMotionListener  {

    /**
	 *
	 */
	private static final long serialVersionUID = 5802250251892627477L;

	/** Determines how fast our image will float */
    private static final int FLOAT_INTERVAL = 90;

    /** The source component that is being dragged */
    private JComponent dragSource;

    /** The image drawn to the screen during the drag */
    private BufferedImage image;

    /**
     * The list of user defined supported components that this {@link DnDIconPainter} can handle.
     * This list is checked before the statically defined one.
     */
    private final List<DraggableComponent> userSupportedComps;

    /**
     * The list of supported components that this {@link DnDIconPainter} can handle.
     */
    private static List<DraggableComponent> supportedComps = new ArrayList<DraggableComponent>();

    /**
     * The offset used when drawing the image.
     * Set as the offset of the user click location into the component
     */
    private Point imgOffset;

    /**
     * The point, in this components coordinate space, where the image will be drawn
     */
    private Point drawingPosition;

    public DnDIconPainter(){

        DragSource.getDefaultDragSource().addDragSourceListener(this);
         DragSource.getDefaultDragSource().addDragSourceMotionListener(this);

         dragSource = null;
         userSupportedComps = new ArrayList<DraggableComponent>();

    }

    /**
     * Set up the image to draw for a new drag on the given component.
     *
     * @param src
     */
    private void setUpDragImage(final Component src){
        if(src != null && src instanceof JComponent){


            dragSource = (JComponent)src;

            imgOffset = dragSource.getMousePosition();
            if(imgOffset == null) {
				imgOffset = new Point(0,0);
			}


            //first check the user defined list
            Area imageClip = null;
            for(DraggableComponent d : userSupportedComps){
                if(d.isOfType(dragSource)){
                    imageClip = d.getClip(dragSource);
                    break;
                }
            }

            //if we dont find a match check our statically defined list
            if(imageClip == null){
                for(DraggableComponent d : supportedComps){
                    if(d.isOfType(dragSource)){
                        imageClip = d.getClip(dragSource);
                        break;
                    }
                }
            }


            image = new BufferedImage(dragSource.getWidth(), dragSource.getHeight(), BufferedImage.TYPE_INT_ARGB);

            Graphics2D g = image.createGraphics();

            if(imageClip != null){
                g.setClip(imageClip);
            }

            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .55f));

            //make the background transparent
            Color old = dragSource.getBackground();
            dragSource.setBackground(new Color(0,0,0,0));
            dragSource.setDoubleBuffered(false);
            dragSource.paint(g);
            dragSource.setDoubleBuffered(true);
            //return to old background
            dragSource.setBackground(old);

        }
    }

    @Override
    public void paint(final Graphics g) {
        Graphics2D g2 = (Graphics2D)g.create();

        Point p = getDrawingPosition();

        if(p != null && image != null){

            g2.drawImage(image, p.x - imgOffset.x, p.y - imgOffset.y, null);

        }
    }

    /**
     * Get the position(in this component space) to draw the image
     *
     * @return
     */
    protected Point getDrawingPosition(){
        return drawingPosition;
    }


    /**
     * Set the postion that the image will be drawn at.
     *
     * @param p
     */
    protected void setDrawingPosition(final Point p){
        drawingPosition = p;
    }


    /**
     * Every time a we get this event we will repaint and make
     * sure our cursor is correct.
     * The first itme we get this event for a drag we will set up
     * the image to display and set ourselves to visible so the image
     * can bee seen.
     */
    public void dragMouseMoved(final DragSourceDragEvent dsde) {
        if(dragSource == null){

            setUpDragImage(dsde.getDragSourceContext().getComponent());
            setVisible(true);
        }
        this.setCursor(dsde.getDragSourceContext().getCursor());
        this.setDrawingPosition(this.getMousePosition());
        this.repaint();
    }


    /**
     * When the drag and drop is over we will float the image
     * back to its starting location.
     */
    public void dragDropEnd(final DragSourceDropEvent dsde) {

        if(dsde.getDropSuccess()){

            endDrag();

        }else{
            //if the drop wasnt a success then we float the image back home

            //determine the start and end locations in local coordinate space
            Point compLoc = getLocationOnScreen();

            Point home = dragSource.getLocationOnScreen();
            home.setLocation(home.x - compLoc.x + imgOffset.x, home.y - compLoc.y + imgOffset.y);

            Point current = dsde.getLocation();
            current.setLocation(current.x - compLoc.x, current.y - compLoc.y);

            floatImage(current, home);

        }
    }

    /**
     * Simulate the image floating from start position to end position.
     * The current image stored for the current drag is used.
     * This method spawns a thread to continually move and repaint the image.
     * When the image reaches its end location the thread will stop and we will
     * hide this entire component.
     * @param start
     * @param end
     */
    private void floatImage(final Point start, final Point end){

        setDrawingPosition(start);

        final Timer myTime = new Timer(FLOAT_INTERVAL, null);

        myTime.addActionListener(new ActionListener(){
            @SuppressWarnings("unused")
            public void actionPerformed(final ActionEvent e) {
                Point current = getDrawingPosition();

                int dx = (end.x - current.x)/3;
                int dy = (end.y - current.y)/3;

//                System.out.println("Current: " + current);
//                System.out.println("DX,DY: " +dx + ", " + dy);

                if(Math.abs(dx) > 5 || Math.abs(dy) > 5){
                    setDrawingPosition(new Point(current.x + dx, current.y + dy));
                    repaint();

                }else{

                    myTime.stop();
                    endDrag();
                }
            }
        });
        myTime.start();

    }

    /**
     * When the drag is over this is called to reset our state.
     */
    final protected void endDrag(){
        dragSource =null;
        image = null;
        setVisible(false);
    }

    /* Unused event handling */

    @SuppressWarnings("unused")
    public void dragEnter( final DragSourceDragEvent dsde) {}

    @SuppressWarnings("unused")
    public void dragOver(final DragSourceDragEvent dsde) {}

    @SuppressWarnings("unused")
    public void dropActionChanged(final DragSourceDragEvent dsde) {}

    @SuppressWarnings("unused")
    public void dragExit(final DragSourceEvent dse) {}

    public boolean addSupportedComponent(final DraggableComponent o) {
        return userSupportedComps.add(o);
    }


    public interface DraggableComponent {
        public boolean isOfType(Component c);
        public Area getClip(Component c);
    }


    static{

        supportedComps.add(new DraggableComponent(){

            public boolean isOfType(final Component c) {
                return (c instanceof CheckBoxTree);
            }

            public Area getClip(final Component c) {
                if(!isOfType(c)) {
					return null;
				}

                Area ret = new Area();
                CheckBoxTree tree = (CheckBoxTree)c;
                TreePath[] sel = tree.getCheckedNodesPaths();

                for(TreePath p : sel){
                    int row = tree.getRowForPath(p);
                    if(row != -1) {
						ret.add(new Area(tree.getRowBounds(row)));
					}
                }
                return ret;
            }
        });

        supportedComps.add(new DraggableComponent(){

            public boolean isOfType(final Component c) {
                return (c instanceof JTree);
            }

            public Area getClip(final Component c) {
                if(!isOfType(c)) {
					return null;
				}

                JTree comp = (JTree)c;
                Area ret = new Area();
                int[] rows = comp.getSelectionRows();

                for(int r : rows){
                    ret.add(new Area(comp.getRowBounds(r)));
                }
                return ret;
            }
        });
    }
}